<pre class="metadata">
Title: KV Storage
Shortname: kv-storage
Repository: WICG/kv-storage
Inline Github Issues: true
Group: WICG
Status: CG-DRAFT
Level: 1
URL: https://wicg.github.io/kv-storage/
Boilerplate: omit conformance, omit feedback-header
Editor: Domenic Denicola, Google https://www.google.com/, d@domenic.me, https://domenic.me/
Abstract: This specification details a high level asynchronous key/value storage API, layered on top of IndexedDB, and as a spiritual successor to the original <span class="non-normative">{{Window/localStorage}}</span>.
!Participate: <a href="https://github.com/WICG/kv-storage">GitHub WICG/kv-storage</a> (<a href="https://github.com/WICG/kv-storage/issues/new">new issue</a>, <a href="https://github.com/WICG/kv-storage/issues?state=open">open issues</a>)
!Commits: <a href="https://github.com/WICG/kv-storage/commits/master/spec.bs">GitHub spec.bs commits</a>
Complain About: accidental-2119 yes, missing-example-ids yes
Indent: 2
Default Biblio Status: current
</pre>

<pre class="anchors">
url: https://html.spec.whatwg.org/multipage/webstorage.html#dom-localstorage; type: attribute; text: localStorage; for: Window; spec: HTML

urlPrefix: https://tc39.github.io/ecma262/#; spec: ECMASCRIPT
  text: Map; url: sec-map-objects; type: interface
  text: Date; url: sec-date-objects; type: interface
  text: %ObjectPrototype%; url: sec-properties-of-the-object-prototype-object; type: interface
  text: CreateDataProperty; url: sec-createdataproperty; type: abstract-op
  text: ObjectCreate; url: sec-objectcreate; type: abstract-op
  text: Construct; url: sec-construct; type: abstract-op
  text: Type; url: sec-ecmascript-data-types-and-values; type: abstract-op
  text: IsArray; url: sec-isarray; type: abstract-op
  text: SetIntegrityLevel; url: sec-setintegritylevel; type: abstract-op
  text: current Realm; url: current-realm; type: dfn
  text: typed array; url: sec-typedarray-objects; type: dfn
  text: Realm; url: realm-record; type: dfn

urlPrefix: https://w3c.github.io/IndexedDB/; spec: INDEXEDDB-2; type: dfn
  text: convert a value to a key
  text: convert a key to a value
  text: close a database connection
  text: unbounded key range
  text: transaction; url: #transaction-concept
  text: connection; for: database; url: #connection
  text: index; url: #index-concept
  for: request
    text: result; url: #request-result
    text: error; url: #request-error
  for: transaction
    text: error; url: #transaction-error
  for: connection
    text: object store set; url: #connection-object-store-set
  for: key
    text: ascending; url: #greater-than
  for: object store
    text: key generator; url: #key-generator
    text: key path; url: #object-store-key-path
    text: name; url: #object-store-name
</pre>

<pre class="link-defaults">
spec: webidl; type: dfn; text: resolve
spec: infra; type: dfn; text: list
spec: webidl; type: dfn; text: new
</pre>

<style>
.selected-text-file-an-issue {
  position: fixed;
  bottom: 0;
  right: 0;
  background: rgba(255, 255, 255, 0.8);
  font-size: smaller;
  padding: 4px 10px;
  z-index: 4;
}

summary {
  cursor: pointer;
}

/* domintro from https://resources.whatwg.org/standard.css */
.domintro {
  position: relative;
  color: green;
  background: #DDFFDD;
  margin: 2.5em 0 2em 0;
  padding: 1.5em 1em 0.5em 2em;
}

.domintro dt, .domintro dt * {
  color: black;
  font-size: inherit;
}
.domintro dd {
  margin: 0.5em 0 1em 2em; padding: 0;
}
.domintro dd p {
  margin: 0.5em 0;
}
.domintro::before {
  content: 'For web developers (non-normative)';
  background: green;
  color: white;
  padding: 0.15em 0.25em;
  font-style: normal;
  position: absolute;
  top: -0.8em;
  left: -0.8em;
}
</style>

<script src="https://resources.whatwg.org/file-issue.js" async></script>

<details class="annoying-warning" open>
  <summary>Not an active specification</summary>
  <p>Work on this specification is currently suspended, as no browser teams (including the Chromium project, which originated the proposal) are currently indicating interest in implementing it.</p>
</details>


<h2 id="intro">Introduction</h2>

<div class="non-normative">

<em>This section is non-normative.</em>

The {{Window/localStorage}} API is widely used, and loved for its simplicity. However, its synchronous nature leads to <a href="https://hacks.mozilla.org/2012/03/there-is-no-simple-solution-for-local-storage/">terrible performance</a> and cross-window synchronization issues.

This specification proposes a new API, called KV storage, which is intended to provide an analogously simple interface, while being asynchronous. Along the way, it embraces some additional goals:

* <strong>Layer on top of Indexed Database.</strong> This avoids introducing a new type of storage for user agents and web developers to manage, and allows an upgrade path to full IndexedDB usage if a web developer outgrows the KV storage interface. [[INDEXEDDB-2]]

* <strong>Modernize the API surface.</strong> Modern key/value stores in the platform, such as the {{Cache}} or {{Headers}} APIs, have aligned around the operation names given by JavaScript's {{Map}}. We follow their example. As a bonus, this allows us to avoid the legacy [=named properties=] feature that the {{Storage}} interface uses.

* <strong>Support isolated storage areas.</strong> {{Window/localStorage}} requires careful namespacing of keys to use robustly in a multi-actor environment. Popular libraries meant to replace it, like <a href="https://localforage.github.io/localForage/">localForage</a>, have included a way to create new storage areas beyond the default one.

<div class="example" id="example-from-html-spec">
  A conversion of <a href="https://html.spec.whatwg.org/multipage/webstorage.html#introduction-15">the HTML Standard's <code>localStorage</code> example</a> to use KV storage might look like the following:

  <xmp highlight="html">
    <p>
      You have viewed this page
      <span id="count">an untold number of</span>
      time(s).
    </p>
    <script type="module">
      (async () => {
        let pageLoadCount = await kvStorage.get("pageLoadCount") || 0;
        ++pageLoadCount;

        document.querySelector("#count").textContent = pageLoadCount;

        await kvStorage.set("pageLoadCount", pageLoadCount);
      })();
    </script>
  </xmp>

  As a side note, observe how, in contrast to the original example which performs up to five storage operations, our example only performs two. Also, it updates the UI as soon as possible, instead of delaying the UI update until we've set the new page load count.

  The KV storage API design can take some credit for this, as by forcing us to explicitly state our <code>await</code> points, it makes it more obvious that we're performing a potentially-expensive storage operation.
</div>

</div>


<h2 id="global">The {{WindowOrWorkerGlobalScope/kvStorage}} global</h2>

<xmp class="idl">
partial interface WindowOrWorkerGlobalScope {
  [SecureContext] readonly attribute KVStorageArea kvStorage;
};
</xmp>

<div class="domintro non-normative" id="module-domintro">
  <dl>
    <dt><code>self . {{WindowOrWorkerGlobalScope/kvStorage}}</code>
    <dd>
      <p>Returns the default storage area. It is a pre-constructed instance of the {{KVStorageArea}} class, meant to be a convenience similar to {{Window/localStorage}}.

      <p>This property is only present in [=secure contexts=], since persistent storage is a powerful feature.
    </dd>
  </dl>
</div>

Every {{WindowOrWorkerGlobalScope}} has an associated <dfn>default KV Storage area</dfn>, which is a {{KVStorageArea}} created in that realm with [=[[DatabaseName]]=] set to "<code>kv-storage:default</code>", [=[[DatabasePromise]]=] initially set to null, and [=[[BackingStoreObject]]=] initially set to null.

The <dfn attribute for="WindowOrWorkerGlobalScope"><code>kvStorage</code></dfn> attribute's getter must return [=this=]'s [=default KV Storage area=].

<h2 id="KVStorageArea" interface lt="KVStorageArea">The <code>KVStorageArea</code> class</h2>

<xmp class="idl">
[SecureContext, Exposed=(Window,Worker)]
interface KVStorageArea {
  constructor(DOMString name);

  Promise<void> set(any key, any value);
  Promise<any> get(any key);
  Promise<void> delete(any key);
  Promise<void> clear();

  async iterable<any, any>;

  [SameObject] readonly attribute object backingStore;
};
</xmp>

Each {{KVStorageArea}} instance must also contain the <dfn>\[[DatabaseName]]</dfn>, <dfn>\[[DatabasePromise]]</dfn>, and <dfn>\[[BackingStoreObject]]</dfn> internal slots. The following is a non-normative summary of their meaning:

<dl class="non-normative">
  <dt>[=[[DatabaseName]]=]
  <dd>A string containing the name of the backing IndexedDB database.
  <dt>[=[[DatabasePromise]]=]
  <dd>A promise for an {{IDBDatabase}} object, lazily initialized when [=perform a database operation|performing any database operation=].
  <dt>[=[[BackingStoreObject]]=]</dt>
  <dd>The object returned by the {{KVStorageArea/backingStore}} getter, cached to ensure identity across gets.</dd>
</dl>

<h3 id="KVStorageArea-constructor" constructor for="KVStorageArea">constructor(|name|)</h3>

<dl class="domintro non-normative">
  <dt><code>|storage| = new {{KVStorageArea/constructor(name)|KVStorageArea}}(|name|)</code>
  <dd>
    <p>Creates a new {{KVStorageArea}} that provides an async key/value store view onto an IndexedDB database named <code>`kv-storage:${name}`</code>.

    <p>This does not actually open or create the database yet; that is done lazily when other methods are called. This means that all other methods can reject with database-related exceptions in failure cases.
</dl>

<div algorithm="KVStorageArea constructor">
  1. Set <b>this</b>.[=[[DatabaseName]]=] to the concatenation of "<code>kv-storage:</code>" and <var ignore> name</var>.
  1. Set <b>this</b>.[=[[DatabasePromise]]=] to null.
  1. Set <b>this</b>.[=[[BackingStoreObject]]=] to null.
</div>

<h3 id="KVStorageArea-set" method for="KVStorageArea">set(|key|, |value|)</h3>

<dl class="domintro non-normative">
  <dt><code>await |storage|.{{KVStorageArea/set()|set}}(|key|, |value|)</code>
  <dd>
    <p>Asynchronously stores the given |value| so that it can later be retrieved by the given |key|.

    <p>Keys have to follow the same restrictions as IndexedDB keys: roughly, a key can be a number, string, array, {{Date}}, {{ArrayBuffer}}, {{DataView}}, [=typed array=], or an array of these. Invalid keys will cause the returned promise to reject with a "{{DataError}}" {{DOMException}}.

    <p>Values can be any value that can be [$StructuredSerializeForStorage|structured-serialized for storage$]. Un-serializable values will cause a "{{DataCloneError}}" {{DOMException}}. The value undefined will cause the corresponding entry to be deleted.

    <p>The returned promise will fulfill with undefined on success.
</dl>

<div algorithm="KVStorageArea set()">
  1. If |key| is not [=allowed as a key=], return [=a promise rejected with=] a "{{DataError}}" {{DOMException}}.
  1. Return the result of [=performing a database operation=] given this object, "<code>readwrite</code>", and the following steps operating on |transaction| and |store|:
    1. If |value| is undefined, then
      1. Perform the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/delete()}} method on |store|, given the argument |key|.
    1. Otherwise,
      1. Perform the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/put()}} method on |store|, given the arguments |value| and |key|.
    1. Let |promise| be [=a new promise=] in the [=relevant Realm=] of <b>this</b>.
    1. [=Add a simple event listener=] to |transaction| for "<code>complete</code>" that [=resolves=] |promise| with undefined.
    1. [=Add a simple event listener=] to |transaction| for "<code>error</code>" that [=rejects=] |promise| with |transaction|'s [=transaction/error=].
    1. [=Add a simple event listener=] to |transaction| for "<code>abort</code>" that [=rejects=] |promise| with |transaction|'s [=transaction/error=].
    1. Return |promise|.
</div>

<h3 id="KVStorageArea-get" method for="KVStorageArea">get(|key|)</h3>

<dl class="domintro non-normative">
  <dt><code>|value| = await |storage|.{{KVStorageArea/get()|get}}(|key|)</code>
  <dd>
    <p>Asynchronously retrieves the value stored at the given |key|, or undefined if there is no value stored at |key|.

    <p>Values retrieved will be [$StructuredDeserialize|structured-deserialized$] from their original form.
</dl>

<div algorithm="KVStorageArea get()">
  1. If |key| is not [=allowed as a key=], return [=a promise rejected with=] a "{{DataError}}" {{DOMException}}.
  1. Return the result of [=performing a database operation=] given this object, "<code>readonly</code>", and the following steps operating on <var ignore>transaction</var> and |store|:
    1. Let |request| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/get()}} method on |store|, given the argument |key|.
    1. Let |promise| be [=a new promise=] in the [=relevant Realm=] of <b>this</b>.
    1. [=Add a simple event listener=] to |request| for "<code>success</code>" that [=resolves=] |promise| with |request|'s [=request/result=].
    1. [=Add a simple event listener=] to |request| for "<code>error</code>" that [=rejects=] |promise| with |request|'s [=request/error=].
    1. Return |promise|.
</div>

<h3 id="KVStorageArea-delete" method for="KVStorageArea">delete(|key|)</h3>

<dl class="domintro non-normative">
  <dt><code>await |storage|.{{KVStorageArea/delete()|delete}}(|key|)</code>
  <dd>
    <p>Asynchronously deletes the entry at the given |key|. This is equivalent to |storage|.{{KVStorageArea/set()|set}}(|key|, undefined).

    <p>The returned promise will fulfill with undefined on success.
</dl>

<div algorithm="KVStorageArea delete()">
  1. If |key| is not [=allowed as a key=], return [=a promise rejected with=] a "{{DataError}}" {{DOMException}}.
  1. Return the result of [=performing a database operation=] given this object, "<code>readwrite</code>", and the following steps operating on |transaction| and |store|:
    1. Perform the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/delete()}} method on |store|, given the argument |key|.
    1. Let |promise| be [=a new promise=] in the [=relevant Realm=] of <b>this</b>.
    1. [=Add a simple event listener=] to |transaction| for "<code>complete</code>" that [=resolves=] |promise| with undefined.
    1. [=Add a simple event listener=] to |transaction| for "<code>error</code>" that [=rejects=] |promise| with |transaction|'s [=transaction/error=].
    1. [=Add a simple event listener=] to |transaction| for "<code>abort</code>" that [=rejects=] |promise| with |transaction|'s [=transaction/error=].
    1. Return |promise|.
</div>

<h3 id="KVStorageArea-clear" method for="KVStorageArea">clear()</h3>

<dl class="domintro non-normative">
  <dt><code>await |storage|.{{KVStorageArea/clear()|clear}}()</code>
  <dd>
    <p>Asynchronously deletes all entries in this storage area.

    <p>This is done by actually deleting the underlying IndexedDB database. As such, it always can be used as a fail-safe to get a clean slate, <a href="#clear-recover-from-changes">as shown below</a>.

    <p>The returned promise will fulfill with undefined on success.
</dl>

<div algorithm="KVStorageArea clear()">
  1. Let |realm| be the [=relevant Realm=] of <b>this</b>.
  1. If <b>this</b>.[=[[DatabasePromise]]=] is not null, return the result of [=promise/reacting=] to <b>this</b>.[=[[DatabasePromise]]=] with fulfillment and rejection handlers that both perform the following steps:
    1. Set <b>this</b>.[=[[DatabasePromise]]=] to null.
    1. Return the result of [=deleting the database=] given <b>this</b>.[=[[DatabaseName]]=] and |realm|.
  1. Otherwise, return the result of [=deleting the database=] given <b>this</b>.[=[[DatabaseName]]=] and |realm|.
</div>

<div algorithm>
  To <dfn lt="deleting the database|delete the database">delete the database</dfn> given a string |name| and a [=Realm=] |realm|:

  1. Let |promise| be [=a new promise=] in |realm|.
  1. Let |request| be the result of performing the steps listed in the description of {{IDBFactory}}'s {{IDBFactory/deleteDatabase()}} method on the <a>current <code>IDBFactory</code></a>, given the argument |name|.
  1. If those steps threw an exception, catch the exception and [=reject=] |promise| with it.
  1. Otherwise:
    1. [=Add a simple event listener=] to |request| for "<code>success</code>" that [=resolves=] |promise| with undefined.
    1. [=Add a simple event listener=] to |request| for "<code>error</code>" that [=rejects=] |promise| with |request|'s [=request/error=].
  1. Return |promise|.
</div>

<div class="example" id="clear-recover-from-changes">
  This method can be used to recover from unexpected modifications to the backing store. For example,

  <xmp highlight="js">
    // This upgrade to version 100 breaks the "cats" storage area: since StorageAreas
    // assume a version of 1, "cats" can no longer be used with KV storage.
    const openRequest = indexedDB.open("kv-storage:cats", 100);
    openRequest.onsuccess = () => {
      openRequest.onsuccess.close();
    };

    (async () => {
      const area = new KVStorageArea("cats");

      // Due to the above upgrade, all other methods will reject:
      try {
        await area.set("fluffy", new Cat());
      } catch (e) {
        // This will be reached and output a "VersionError" DOMException
        console.error(e);
      }

      // But clear() will delete the database entirely:
      await area.clear();

      // Now we can use it again!
      await area.set("fluffy", new Cat());
      await area.set("tigger", new Cat());

      // Also, the version is back down to 1:
      console.assert(area.backingStore.version === 1);
    })();
  </xmp>
</div>

<h3 id="KVStorageArea-iteration">Iteration</h3>

The {{KVStorageArea}} interface supports asynchronous iteration.

<div class="domintro non-normative">
  <dl>
    <dt><code>for await (const |key| of |storage|.<dfn method for="KVStorageArea">keys()</dfn>) { ... }</code>
    <dd>
      <p>Retrieves an async iterator containing the keys of all entries in this storage area.

      <p>Keys will be yielded in [=key/ascending=] order; roughly, segregated by type, and then sorted within each type. They will be [=key round-tripped=] from their original form.

    <dt><code>for await (const |value| of |storage|.<dfn method for="KVStorageArea">values()</dfn>) { ... }</code>
    <dd>
      <p>Retrieves an async iterator containing the values of all entries in this storage area.

      <p>Values will be yielded in the same order as for {{KVStorageArea/keys()}}. They will be [$StructuredDeserialize|structured-deserialized$] from their original form.

    <dt><code>for await (const [|key|, |value|] of |storage|.<dfn method for="KVStorageArea">entries()</dfn>) { ... }</code>
    <dt><code>for await (const [|key|, |value|] of |storage|) { ... }</code>
    <dd>
      <p>Retrieves an async iterator containing two-element <code>[key, value]</code> arrays, each of which corresponds to an entry in this storage area.

      <p>Entries will be yielded in the same order as for {{KVStorageArea/keys()}}. Each key and value will be [=key round-tripped=] and [$StructuredDeserialize|structured-deserialized$] from its original form, respectively.
  </dl>
  <p>All of these iterators provide live views onto the storage area: modifications made to entries sorted after the last-returned one will be reflected in the iteration.
</div>

<div algorithm="asynchronous iterator initialization steps">
  The [=asynchronous iterator initialization steps=] for {{KVStorageArea}}, given <var ignore>storageArea</var> and |asyncIterator|, are:

  1. Set |asyncIterator|'s <dfn for="KVStorageAreaAsyncIterator">last key</dfn> to [=not yet started=].
</div>

<div algorithm="get the next iteration result">
  To [=get the next iteration result=], given the |storageArea| and |asyncIterator|:

  1. Return the result of [=performing a database operation=] given |storageArea|, "<code>readonly</code>", and the following steps operating on <var ignore>transaction</var> and |store|:
    1. Let |range| be the result of [=getting the range for=] |asyncIterator|'s [=KVStorageAreaAsyncIterator/last key=].
    1. Let |keyRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/getKey()}} method on |store|, given the argument |range|.
    1. Let |valueRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/get()}} method on |store|, given the argument |range|.

        Note: The iterator returned from {{KVStorageArea/keys()}} discards the value. Implementations could avoid constructing |valueRequest| in that case.
    1. Let |promise| be [=a new promise=] in the [=relevant Realm=] of |asyncIterator|.
    1. [=Add a simple event listener=] to |valueRequest| for "<code>success</code>" that performs the following steps:
      1. Let |key| be |keyRequest|'s [=request/result=].
      1. If |key| is undefined, then:
        1. [=Resolve=] |promise| with undefined.
      1. Otherwise:
        1. Let |value| be |valueRequest|'s [=request/result=].
        1. [=Resolve=] |promise| with (|key|, |value|, |key|).
        1. Set |asyncIterator|'s [=KVStorageAreaAsyncIterator/last key=] to |key|.
    1. [=Add a simple event listener=] to |keyRequest| for "<code>error</code>" that [=rejects=] |promise| with |keyRequest|'s [=request/error=].
    1. [=Add a simple event listener=] to |valueRequest| for "<code>error</code>" that [=rejects=] |promise| with |valueRequest|'s [=request/error=].
    1. Return |promise|.
</div>

<div class="example" id="example-live-async-iterator">
  To illustrate the live nature of the async iterators, consider the following:

  <xmp highlight="js">
    await kvStorage.set(10, "value 10");
    await kvStorage.set(20, "value 20");
    await kvStorage.set(30, "value 30");

    const keysSeen = [];
    for await (const key of kvStorage.keys()) {
      if (key === 20) {
        await kvStorage.set(15, "value 15");
        await kvStorage.delete(20);
        await kvStorage.set(25, "value 25");
      }
      keysSeen.push(key);
    }

    console.log(keysSeen);   // logs 10, 20, 25, 30
  </xmp>

  That is, calling {{KVStorageArea/keys()}} does not create a snapshot as of the time it was called; it returns a live asynchronous iterator, that lazily retrieves the next key after the last-seen one.
</div>

<div class="example" id="example-entries-to-server">
  Assuming you knew that that you only stored JSON-compatible types in the {{KVStorageArea}} |storage|, you could use the following code to send all locally-stored entries to a server:

  <xmp highlight="js">
    const entries = [];
    for await (const entry of kvStorage.entries()) {
      entries.push(entry);
    }

    fetch("/storage-receiver", {
      method: "POST",
      body: entries,
      headers: {
        "Content-Type": "application/json"
      }
    });
  </xmp>
</div>

<h3 id="KVStorageArea-backingstore" attribute for="KVStorageArea" lt="backingStore">backingStore</h3>

<dl class="domintro non-normative">
  <dt><code>{ |database|, |store|, |version| } = |storage|.{{KVStorageArea/backingStore}}</code>
  <dd>
    <p>Returns an object containing all of the information necessary to manually interface with the IndexedDB backing store that underlies this storage area:

    * |database| will be a string equal to "<code>kv-storage:</code>" concatenated with the database name passed to the constructor. (For the default storage area, it will be "<code>kv-storage:default</code>".)
    * |store| will be the string "<code>store</code>".
    * |version| will be the number 1.

    <p>It is good practice to use the {{KVStorageArea/backingStore}} property to retrieve this information, instead of memorizing the above factoids.
</dl>

<div algorithm="StorageaArea backingStore">
  1. If <b>this</b>.[=[[BackingStoreObject]]=] is null, then:
    1. Let |backingStoreObject| be [$ObjectCreate$]({{%ObjectPrototype%}}).
    1. Perform [$CreateDataProperty$](|backingStoreObject|, "<code>database</code>", <b>this</b>.[=[[DatabaseName]]=]).
    1. Perform [$CreateDataProperty$](|backingStoreObject|, "<code>store</code>", "<code>store</code>").
    1. Perform [$CreateDataProperty$](|backingStoreObject|, "<code>version</code>", 1).
    1. Perform [$SetIntegrityLevel$](|backingStoreObject|, "<code>frozen</code>").
    1. Set <b>this</b>.[=[[BackingStoreObject]]=] to |backingStoreObject|.
  1. Return <b>this</b>.[=[[BackingStoreObject]]=].
</div>

<div class="example" id="example-backingstore">
  Consider a checklist application, which tracks the Pokémon a user has collected. It might use a {{KVStorageArea}} |storage| like so:

  <xmp highlight="js">
    bulbasaur.onchange = () => kvStorage.set("bulbasaur", bulbasaur.checked);
    ivysaur.onchange = () => kvStorage.set("ivysaur", ivysaur.checked);
    venusaur.onchange = () => kvStorage.set("venusaur", venusaur.checked);
    // ...
  </xmp>

  (Hopefully the developer quickly realizes that the above will be hard to maintain, and refactors the code into a loop. But in the meantime, their repetitive code makes for a good example, so let's take advantage of that.)

  The developer now realizes they want to add an evolution feature, e.g. for when the user transforms their Bulbasaur into an Ivysaur. They might first implement this like so:

  <xmp highlight="js">
    bulbasaurEvolve.onclick = async () => {
      await kvStorage.set("bulbasaur", false);
      await kvStorage.set("ivysaur", true);
    };
  </xmp>

  However, our developer starts getting bug reports from their users: if the users happen to open up the checklist app in a second tab while they're evolving in the first tab, the second tab will sometimes see that their Bulbasaur has disappeared, without ever turning into an Ivysaur! A Pokémon has gone missing!

  The solution here is to step beyond the comfort zone of KV storage, and start using the full power of IndexedDB: in particular, its [=transactions=] feature. The {{KVStorageArea/backingStore}} getter is the gateway to this world:

  <xmp highlight="js">
    const { database, store, version } = kvStorage.backingStore;
    const request = indexedDB.open(database, version);
    request.onsuccess = () => {
      const db = request.result;

      bulbasaurEvolve.onclick = () => {
        const transaction = db.transaction(store, "readwrite");
        const store = transaction.objectStore(store);

        store.put("bulbasaur", false);
        store.put("ivysaur", true);

        db.close();
      };
    };
  </xmp>

  Satisfied with their web app's Pokémon integrity, our developer is now happy and fulfilled. (At least, until they realize that none of their code has error handling.)
</div>

<h2 id="supporting">Supporting operations and concepts</h3>

<div algorithm>
  To <dfn>add a simple event listener</dfn>, given an {{EventTarget}} |target|, an event type string |type|, and a set of steps |steps|:

  1. Let |jsCallback| be a new JavaScript function object, created in the [=current realm=], that performs the steps given by |steps|. Other properties of the function (such as its <code>name</code> and <code>length</code> properties, or \[[Prototype]]) are unobservable, and can be chosen arbitrarily.
  1. Let |idlCallback| be the result of [=converted to an IDL value|converting=] |jsCallback| to an {{EventListener}}.
  1. Perform the steps listed in the description of {{EventTarget}}'s {{EventTarget/addEventListener()}} method on |target| given the arguments |type| and |idlCallback|.
</div>

<div algorithm>
  The <dfn>current <code>IDBFactory</code></dfn> is the {{IDBFactory}} instance returned by the following steps:

  1. Assert: the [=current global object=] [=includes=] {{WindowOrWorkerGlobalScope}}.
  1. Return the result of performing the steps listed in the description of the getter for {{WindowOrWorkerGlobalScope}}'s {{WindowOrWorkerGlobalScope/indexedDB}} attribute on the [=current global object=].
</div>

<div algorithm>
  To <dfn lt="performing a database operation|perform a database operation">perform a database operation</dfn> given a {{KVStorageArea}} |area|, a mode string |mode|, and a set of steps |steps| that operate on an {{IDBTransaction}} |transaction| and an {{IDBObjectStore}} |store|:

  1. Assert: |area|.[=[[DatabaseName]]=] is a string (and in particular is not null).
  1. If |area|.[=[[DatabasePromise]]=] is null, [=initialize the database promise=] for |area|.
  1. Return the result of [=promise/reacting=] to |area|.[=[[DatabasePromise]]=] with a fulfillment handler that performs the following steps, given |database|:
    1. Let |transaction| be the result of performing the steps listed in the description of {{IDBDatabase}}'s {{IDBDatabase/transaction()}} method on |database|, given the arguments "<code>store</code>" and |mode|.
    1. Let |store| be the result of performing the steps listed in the description of {{IDBTransaction}}'s {{IDBTransaction/objectStore()}} method on |transaction|, given the argument "<code>store</code>".
    1. Return the result of performing |steps|, passing along |transaction| and |store|.
</div>

<div algorithm>
  To <dfn>initialize the database promise</dfn> for a {{KVStorageArea}} |area|:

  1. Set |area|.[=[[DatabasePromise]]=] to [=a new promise=] in the [=relevant Realm=] of |area|.
  1. If the [=current global object=] does not [=include=] {{WindowOrWorkerGlobalScope}}, [=reject=] |area|.[=[[DatabasePromise]]=] with a {{TypeError}}, and return.
  1. Let |request| be the result of performing the steps listed in the description of {{IDBFactory}}'s {{IDBFactory/open()}} method on the <a>current <code>IDBFactory</code></a>, given the arguments |area|.[=[[DatabaseName]]=] and 1.
  1. If those steps threw an exception, catch the exception, [=reject=] |area|.[=[[DatabasePromise]]=] with it, and return.
  1. [=Add a simple event listener=] to |request| for "<code>success</code>" that performs the following steps:
    1. Let |database| be |request|'s [=request/result=].
    1. [=Check the database schema=] for |database|. If the result is false, [=reject=] |area|.[=[[DatabasePromise]]=] with an "{{InvalidStateError}}" {{DOMException}} and abort these steps.
    1. [=Add a simple event listener=] to |database| for "<code>close</code>" that sets |area|.[=[[DatabasePromise]]=] to null.
      <p class="note">This means that if the database is [=close a database connection|closed abnormally=], future invocations of [=perform a database operation=] will attempt to reopen it.</p>
    1. [=Add a simple event listener=] to |database| for "<code>versionchange</code>" that performs the steps listed in the description of {{IDBDatabase}}'s {{IDBDatabase/close()}} method on |database|, and then sets |area|.[=[[DatabasePromise]]=] to null.
        <p class="note">This allows attempts to upgrade the underlying database, or to delete it (e.g. via the {{KVStorageArea/clear()}} method), to succeed. Without this, if two {{KVStorageArea}} instances were both open referencing the same underlying database, {{KVStorageArea/clear()}} would hang, as it only closes the connection maintained by the {{KVStorageArea}} it is invoked on.</p>
    1. [=Resolve=] |promise| with |database|.
  1. [=Add a simple event listener=] to |request| for "<code>error</code>" that [=rejects=] |promise| with |request|'s [=request/error=].
  1. [=Add a simple event listener=] to |request| for "<code>upgradeneeded</code>" that performs the following steps:
    1. Let |database| be |request|'s [=request/result=].
    1. Perform the steps listed in the description of {{IDBDatabase}}'s {{IDBDatabase/createObjectStore()}} method on |database|, given the arguments "<code>store</code>".
    1. If these steps throw an exception, catch the exception and [=reject=] |area|.[=[[DatabasePromise]]=] with it.
</div>

<div algorithm>
  To <dfn>check the database schema</dfn> for an {{IDBDatabase}} |database|:

  1. Let |objectStores| be |database|'s [=database/connection=]'s [=connection/object store set=].
  1. If |objectStores|'s [=set/size=] is not 1, return false.
  1. Let |store| be |objectStores|[0].
  1. If |store|'s [=object store/name=] is not "<code>store</code>", return false.
  1. If |store| has a [=object store/key generator=], return false.
  1. If |store| has a [=object store/key path=], return false.
  1. If any [=indexes=] reference |store|, return false.
  1. Return true.

  <p class="note">[=Check the database schema=] only needs to be called in the initial setup algorithm, [=initialize the database promise=], since once the database connection has been opened, the schema cannot change.</p>
</div>

<div algorithm>
  A value |value| is <dfn>allowed as a key</dfn> if the following steps return true:

  1. If [$Type$](|value|) is Number or String, return true.
  1. If [$IsArray$](|value|) is true, return true.
  1. If |value| has a \[[DateValue]] internal slot, return true.
  1. If |value| has a \[[ViewedArrayBuffer]] internal slot, return true.
  1. If |value| has an \[[ArrayBufferByteLength]] internal slot, return true.
  1. Return false.

  <div class="note">
    A value being [=allowed as a key=] means that it can at least plausibly be used as a key in the IndexedDB APIs. In particular, the values which are [=allowed as a key=] are a subset of those for which IndexedDB's [=convert a value to a key=] algorithm will succeed.

    Most notably, using the [=allowed as a key=] predicate ensures that {{IDBKeyRange}} objects, or any other special object that is accepted as a query in future IndexedDB specification revisions, will be disallowed. Only straightforward key values are accepted by the KV storage API.
  </div>
</div>

<dfn lt="key round-trip">Key round-tripping</dfn> refers to the way in which JavaScript values are processed by first being passed through IndexedDB's [=convert a value to a key=] operation, then converted back through its [=convert a key to a value=] operation. Keys returned by the {{KVStorageArea/keys()}} or {{KVStorageArea/entries()}} methods will have gone through this process.

Notably, any [=typed arrays=] or {{DataView}}s will have been "unwrapped", and returned back as just {{ArrayBuffer}}s containing the same bytes. Also, similar to the [$StructuredSerializeForStorage|structured-serialization$]/[$StructuredDeserialize|deserialization$] process, any "expando" properties or other modifications will not be preserved by [=key round-tripping=].

<p class="note">For primitive string or number values, there's no need to worry about [=key round-tripping=]; the values are indistinguishable.</p>

<div algorithm>
  To <dfn lt="get the range for a key|getting the range for|getting the range for a key">get the range for a key</dfn> |key|:

  1. If |key| is [=not yet started=], then return the result of performing the steps listed in the description of the {{IDBKeyRange/lowerBound(lower)|IDBKeyRange.lowerBound()}} static method, given the argument &minus;Infinity.
    <p class="note">The intent here is to get an [=unbounded key range=], but this is the closest thing we can get that is representable as an {{IDBKeyRange}} object. It works equivalently for our purposes, but will behave incorrectly if Indexed DB ever adds keys that sort below &minus;Infinity. See <a href="https://github.com/WICG/kv-storage/issues/6#issuecomment-452054944">some discussion on potential future improvements</a>.</p>
  1. Otherwise, return the result of performing the steps listed listed in the description of the {{IDBKeyRange/lowerBound(lower, open)|IDBKeyRange.lowerBound()}} static method, given the arguments |key| and true.
</div>

The special value <dfn>not yet started</dfn> can be taken to be any JavaScript value that is not equal to any other program-accessible JavaScript value (but is equal to itself). It is used exclusively as an argument to the [=get the range for a key=] algorithm.

<p class="example" id="example-not-yet-started">A newly created object or symbol, e.g. <code highlight="js">const nys = {}</code> or <code highlight="js">const nys = Symbol()</code>, would satisfy this definition.</code></p>

<h2 id="acks" class="no-num">Acknowledgments</h2>

The editor would like to thank
Andrew Sutherland,
Kenneth Rohde Christiansen,
Jacob Rask,
Jake Archibald,
Jan Varga,
Joshua Bell,
Ms2ger,
and
Victor Costan
for their contributions to this specification.

<h2 id="conformance" class="no-num">Conformance</h2>

This specification depends on the Infra Standard. [[!INFRA]]
